package com.ke.rule.base;

import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.base.Stopwatch;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import org.reflections.util.ConfigurationBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 重写类实例工厂
 * 
 * @author guyu
 *
 */
public final class OverwrittenFactory {
  private static final Logger logger = LoggerFactory.getLogger(OverwrittenFactory.class);
  
  private static final ConcurrentMap<String, Reflections> classInfos = new ConcurrentHashMap<>();
  private static final ReentrantLock lock = new ReentrantLock();

  /**
   * 获取指定包名下的所有类
   * @param packageName
   * @return
   */
  private static Reflections reflection4Package(String packageName) {
    Stopwatch watch = Stopwatch.createStarted();
    
    Reflections reflections = classInfos.get(packageName);
    if (null == reflections) {
      lock.lock();
      try {
        reflections = classInfos.get(packageName);
        if (null == reflections) {
          reflections = new Reflections(
              new ConfigurationBuilder().forPackage(packageName).setScanners(Scanners.SubTypes));
          classInfos.putIfAbsent(packageName, reflections);
          
          // dump出scan的结果
          Set<Class<? extends IServiceMethod>> subTypes = reflections.getSubTypesOf(IServiceMethod.class);
          logger.info("subtypes of IServiceMethod: {}", subTypes.stream().map(Class::getSimpleName).collect(Collectors.joining(", ")));
        }
      } finally {
        lock.unlock();
      }
      logger.info("reflection4Package({}) elapsed: {} ms", packageName, watch.elapsed(TimeUnit.MILLISECONDS));
    }
    
    return reflections;
  }
  
  /**
   * 获取领域层包名
   * @param serviceImpl
   * @return
   */
  private static String getDomainLayerPackageName(IApplicationService serviceImpl) {
    String packageName = ClassUtils.getPackageName(serviceImpl, "");
    String domainPkgName = StringUtils.substringBefore(packageName, ".service");
    domainPkgName += ".domain";
    if(domainPkgName.contains(".generated.")) {
      domainPkgName = domainPkgName.replace(".generated.", ".");
    }
    return domainPkgName;
  }
  
  /**
   * 创建方法实现的实例，若存在子类则优先加载子类实现（使用BiFunction来筛选合适的子类），无子类时默认返回参数指定方法类的实例
   * @param <R>
   * @param serviceImpl 服务实例
   * @param methodImplClass 方法实现类，无子类时默认返回此类的实例
   * @param typeRef 返回值类型
   * @param biFunction 筛选函数
   * @param args 构造函数的参数
   * @return
   */
  @SuppressWarnings({"rawtypes", "unchecked"})
  private static <R> ServiceMethodTemplate<R> makeMethodImpl(IApplicationService serviceImpl,
      Class<? extends ServiceMethodTemplate> methodImplClass, TypeReference<R> typeRef,
      BiFunction<Class<?>, Set<? extends Class<? extends ServiceMethodTemplate>>, Class<? extends ServiceMethodTemplate>> biFunction,
      Object... args) {
    Stopwatch stopwatch = Stopwatch.createStarted();
    
    Class<? extends ServiceMethodTemplate> methodImplSubclass = null;
    ServiceMethodTemplate<R> methodInstance = null;
    try {
      Reflections reflections = reflection4Package(getDomainLayerPackageName(serviceImpl));
      Set<? extends Class<? extends ServiceMethodTemplate>> subTypes = reflections.getSubTypesOf(methodImplClass);
      
      if (!subTypes.isEmpty()) {
        methodImplSubclass = biFunction.apply(serviceImpl.getClass(), subTypes);
      }
      if (null == methodImplSubclass) {
        methodImplSubclass = methodImplClass;
      }

      // build actual parameters
      List<Object> initargs = new ArrayList<>();
      initargs.add(serviceImpl);
      if (null != args && args.length > 0) {
        initargs.addAll(Arrays.asList(args));
      }
      methodInstance = (ServiceMethodTemplate<R>) ConstructorUtils.invokeConstructor(methodImplSubclass, initargs.toArray(new Object[0]));
    } catch (Exception e) {
      logger.error("newMethodImpl(" + methodImplClass.getName() + ") error", e);
      
      if (e instanceof InvocationTargetException) {
        Throwable target = ((InvocationTargetException) e).getTargetException();
        ExceptionUtils.rethrow(target);
      } else {
        ExceptionUtils.rethrow(e);
      }
    } finally {
      if (logger.isDebugEnabled()) {
        logger.debug("overridden method class: {} {}, duration: {} ms",
            methodImplSubclass.getSimpleName(),
            null == methodInstance ? "failed" : "done",
            stopwatch.elapsed(TimeUnit.MILLISECONDS));
      }
    }
    return methodInstance;
  }
  
  /**
   * 非实体服务的方法工厂
   * @param <R>
   * @param serviceImpl
   * @param methodImplClass
   * @param typeRef
   * @param args
   * @return
   */
  @SuppressWarnings("rawtypes")
  public static <R> ServiceMethodTemplate<R> newMethodImpl(IApplicationService serviceImpl,
      Class<? extends ServiceMethodBase> methodImplClass,
      TypeReference<R> typeRef, Object... args) {
    BiFunction<Class<?>, Set<? extends Class<? extends ServiceMethodTemplate>>, Class<? extends ServiceMethodTemplate>> biFunction = (srvKlass, methodSubTypes) -> {
      // 取首个非匿名子类
      return methodSubTypes.stream().filter(subType -> !subType.getName().contains("$")).findFirst().orElse(null);
    };
    return makeMethodImpl(serviceImpl, methodImplClass, typeRef, biFunction, args);
  }
  
  /**
   * 按约定规范获取决策表的路径
   * <br>
   * 规则表所在目录的路径约定规范：rules/服务类名称去掉Service关键字后转为小写
   * 
   * @param serviceImplClass
   * @param folderName 空则取service所在路径
   * @param dtableFileName
   * @return 文件不存在则返回空，否则返回绝对路径
   */
  public static String getDecisionTableFile(Class<? extends IApplicationService> serviceImplClass,
      String folderName, String dtableFileName) {
    if (StringUtils.isBlank(folderName)) {

      String serviceImplName = serviceImplClass.getSimpleName();
      String separator = (serviceImplName.endsWith("Template") || serviceImplName.endsWith("TemplateImpl")) ? "Template": "Service";
      folderName = StringUtils.substringBefore(serviceImplClass.getSimpleName(), separator); // Service, ServiceImpl etc
      if (isNonLowerCaseDirExist(serviceImplClass, folderName)) {
        throw new IllegalArgumentException(String.format("The directory name(%s) must be all lowercase", folderName));
      }
    }
    
    String filePath =  String.format("%s/%s/%s", DecisionTables.DECISION_TABLE_DIR, folderName.toLowerCase(), dtableFileName);
    return null == serviceImplClass.getResource(filePath) ? null : filePath;
  }
  
  /**
   * 非小写的目录名称是否存在？
   * 
   * mac或windows下目录不区分大小写，但linux严格区分，开发过程中偶发因目录存在大些字母导致本机开发环境正常，测试环境找不到决策表的问题
   * 故对目录存在大些字母的情况进行检查
   * 
   * @param clazz
   * @param dirName
   * @return exists?
   */
  private static boolean isNonLowerCaseDirExist(Class<? extends IApplicationService> clazz, String dirName) {
    boolean exists = false;
    
    File rulesDir = null;
    try {
      URI uri = clazz.getResource(DecisionTables.DECISION_TABLE_DIR).toURI();
      if (!uri.isOpaque()) {    // 如果已经被打成jar包则无法使用File来访问目录
        rulesDir = new File(clazz.getResource(DecisionTables.DECISION_TABLE_DIR).toURI());
      }
    } catch (Exception e) {
      logger.warn("getResource(" + DecisionTables.DECISION_TABLE_DIR + ").toURI error", e);
    }
    
    if (null != rulesDir) {
      String[] subdirs = rulesDir.list(DirectoryFileFilter.INSTANCE);
      exists = Stream.of(subdirs).anyMatch(name -> dirName.equalsIgnoreCase(name) && !StringUtils.isAllLowerCase(name));
    }
    
    return exists;
  }
}
